// Copyright (c) 2007 Readify Pty. Ltd.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Diagnostics;
using BuildVirtualizerProxy.Properties;

namespace BuildVirtualizerProxy
{
    public class BuildMachineManager
    {
        private static TraceSwitch m_TraceSwitch = new TraceSwitch("General", string.Empty);

        private static BuildMachineManager m_Current;

        public static BuildMachineManager Current
        {
            get
            {
                if (BuildMachineManager.m_Current == null)
                {
                    BuildMachineManager.m_Current = new BuildMachineManager();
                    BuildMachineManager.m_Current.Start();
                }

                return BuildMachineManager.m_Current;
            }
        }

        private Queue<Reservation> m_Queue = new Queue<Reservation>();
        private Reservation m_CurrentReservation;

        private void Worker()
        {
            while (true)
            {
                if (this.m_Queue.Count != 0)
                {
                    try
                    {
                        this.m_CurrentReservation = this.m_Queue.Dequeue();
                        this.ProcessReservation(this.m_CurrentReservation);
                        this.m_CurrentReservation = null;
                    }
                    catch (Exception ex)
                    {
                        #region Tracing
#line hidden
                        if (BuildMachineManager.m_TraceSwitch.TraceError)
                        {
                            Trace.TraceError(
                                "Failed to process reservation cleanly the exception was:\n{0}",
                                ex
                                );
                        }
#line default
                        #endregion
                    }
                }
                else
                {
                    Thread.Sleep(1000);
                }
            }
        }

        private void ProcessReservation(Reservation reservation)
        {
            #region Tracing
#line hidden
            if (BuildMachineManager.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Processing reservation." +
                    "\nTicket = '{0}'" +
                    "\nLast Inquiry = '{1}'" +
                    "\nRelease By Date = '{2}'" +
                    "\nReservation Status = '{3}'" +
                    "\nBuild Machine = '{4}'",
                    reservation.Ticket,
                    reservation.LastInquiry,
                    reservation.ReleaseByDate,
                    reservation.Status,
                    reservation.BuildMachine
                    );
            }
#line default
            #endregion

            if (HasReachedInquiryTimedOut(reservation))
            {
                #region Tracing
#line hidden
                if (BuildMachineManager.m_TraceSwitch.TraceInfo)
                {
                    Trace.TraceWarning(
                        "Reservation was orphaned in the queue." +
                        "\nTicket = '{0}'" +
                        "\nLast Inquiry = '{1}'" +
                        "\nRelease By Date = '{2}'" +
                        "\nReservation Status = '{3}'" +
                        "\nBuild Machine = '{4}'",
                        reservation.Ticket,
                        reservation.LastInquiry,
                        reservation.ReleaseByDate,
                        reservation.Status,
                        reservation.BuildMachine
                        );
                }
#line default
                #endregion

                return;
            }

            this.EnsureCleanState(false);
            this.EnsureValidBuildServer(reservation);
            this.StartBuildMachine(reservation.BuildMachine);
            reservation.ReleaseByDate = DateTime.Now + Settings.Default.ReservationTimeout;
            reservation.Status = ReservationStatus.Ready;

            #region Tracing
#line hidden
            if (BuildMachineManager.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Reservation status updated." +
                    "\nTicket = '{0}'" +
                    "\nLast Inquiry = '{1}'" +
                    "\nRelease By Date = '{2}'" +
                    "\nReservation Status = '{3}'" +
                    "\nBuild Machine = '{4}'",
                    reservation.Ticket,
                    reservation.LastInquiry,
                    reservation.ReleaseByDate,
                    reservation.Status,
                    reservation.BuildMachine
                    );
            }
#line default
            #endregion

            while (true)
            {
                #region Tracing
#line hidden
                if (BuildMachineManager.m_TraceSwitch.TraceInfo)
                {
                    Trace.TraceInformation(
                        "Checking for reservation inquiry (heartbeat)." +
                        "\nTicket = '{0}'" +
                        "\nLast Inquiry = '{1}'" +
                        "\nRelease By Date = '{2}'" +
                        "\nReservation Status = '{3}'" +
                        "\nBuild Machine = '{4}'",
                        reservation.Ticket,
                        reservation.LastInquiry,
                        reservation.ReleaseByDate,
                        reservation.Status,
                        reservation.BuildMachine
                        );
                }
#line default
                #endregion


                if ((this.m_CurrentReservation.Status == ReservationStatus.Released) ||
                    this.HasReachedInquiryTimedOut(reservation) ||
                    this.HasReachedReservationTimedOut(reservation))
                {
                    this.EnsureCleanState(true);
                    return;
                }

                Thread.Sleep(Settings.Default.PollPeriod);
            }
        }

        private void StartBuildMachine(string buildMachine)
        {
            VirtualServerInterop.RestoreBuildMachine(buildMachine);
        }

        private void EnsureValidBuildServer(Reservation reservation)
        {
            #region Tracing
#line hidden
            if (BuildMachineManager.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Checking requested build machine is valid." +
                    "\nTicket = '{0}'" +
                    "\nLast Inquiry = '{1}'" +
                    "\nRelease By Date = '{2}'" +
                    "\nReservation Status = '{3}'" +
                    "\nBuild Machine = '{4}'",
                    reservation.Ticket,
                    reservation.LastInquiry,
                    reservation.ReleaseByDate,
                    reservation.Status,
                    reservation.BuildMachine
                    );
            }
#line default
            #endregion

            bool exists = Array.Exists<string>(
                Settings.Default.BuildMachines,
                delegate(string item) { return item.ToLower() == reservation.BuildMachine.ToLower(); }
                );

            if (!exists) throw new Exception("Reservation for this build machine cannot be serviced by this proxy.");
            
        }

        private bool HasReachedInquiryTimedOut(Reservation reservation)
        {
            TimeSpan timeSinceLastInquiry = DateTime.Now - reservation.LastInquiry;
            if (timeSinceLastInquiry > Settings.Default.InquiryTimeout) return true;
            return false;
        }

        private bool HasReachedReservationTimedOut(Reservation reservation)
        {
            if (DateTime.Now > reservation.ReleaseByDate) return true;
            return false;
        }

        private void EnsureCleanState(bool shouldBeRunning)
        {
            #region Tracing
#line hidden
            if (BuildMachineManager.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Ensuring clean state." +
                    "\nTicket = '{0}'" +
                    "\nLast Inquiry = '{1}'" +
                    "\nRelease By Date = '{2}'" +
                    "\nReservation Status = '{3}'" +
                    "\nBuild Machine = '{4}'",
                    this.m_CurrentReservation.Ticket,
                    this.m_CurrentReservation.LastInquiry,
                    this.m_CurrentReservation.ReleaseByDate,
                    this.m_CurrentReservation.Status,
                    this.m_CurrentReservation.BuildMachine
                    );
            }
#line default
            #endregion

            foreach (string buildMachine in Settings.Default.BuildMachines)
            {
                VirtualServerInterop.SaveBuildMachine(buildMachine, shouldBeRunning);
                this.RollbackDifferencingDisk(buildMachine);
            }
        }

        private void RollbackDifferencingDisk(string buildMachine)
        {
            #region Tracing
#line hidden
            if (BuildMachineManager.m_TraceSwitch.TraceWarning)
            {
                Trace.TraceWarning("Differencing disk rollback has not been implemented.");
            }
#line default
            #endregion
        }

        private Thread m_WorkerThread;

        public void Start()
        {
            #region Tracing
#line hidden
            if (BuildMachineManager.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation("Starting build machine manager.");
            }
#line default
            #endregion

            if (this.m_WorkerThread != null)
            {
                throw new Exception("The manager has already been started.");
            }

            this.m_WorkerThread = new Thread(this.Worker);
            this.m_WorkerThread.IsBackground = true;
            this.m_WorkerThread.Start();
        }

        public void Stop()
        {
            #region Tracing
#line hidden
            if (BuildMachineManager.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation("Stopping build machine manager.");
            }
#line default
            #endregion

            if (this.m_WorkerThread == null)
            {
                throw new Exception("The manager was not started.");
            }

            this.m_WorkerThread.Abort();
            this.m_WorkerThread = null;
        }

        public string Reserve(string buildServer)
        {
            Reservation reservation = new Reservation();
            reservation.Ticket = Guid.NewGuid().ToString();
            reservation.BuildMachine = buildServer;

            #region Tracing
#line hidden
            if (BuildMachineManager.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Granting reservation.\nQueue Length = '{0}'\nTicket = '{1}'\nBuild Machine = '{2}'",
                    this.m_Queue.Count,
                    reservation.Ticket,
                    reservation.BuildMachine
                    );
            }
#line default
            #endregion

            this.m_Queue.Enqueue(reservation);

            return reservation.Ticket;
        }

        public ReservationStatus Inquire(string ticket)
        {
            foreach (Reservation reservation in this.m_Queue)
            {
                if (reservation.Ticket == ticket)
                {
                    #region Tracing
#line hidden
                    if (BuildMachineManager.m_TraceSwitch.TraceInfo)
                    {
                        Trace.TraceInformation(
                            "Received inquiry. Issued ticket is still in queue.\nQueue Length = '{0}'\nTicket = '{1}'\nBuild Machine = '{2}'",
                            this.m_Queue.Count,
                            reservation.Ticket,
                            reservation.BuildMachine
                            );
                    }
#line default
                    #endregion

                    reservation.LastInquiry = DateTime.Now;
                    return reservation.Status;
                }
            }

            if (this.m_CurrentReservation != null && this.m_CurrentReservation.Ticket == ticket)
            {
                #region Tracing
#line hidden
                if (BuildMachineManager.m_TraceSwitch.TraceInfo)
                {
                    Trace.TraceInformation(
                        "Received inquiry. Issued ticket is now being processed.\nQueue Length = '{0}'\nTicket = '{1}'\nBuild Machine = '{2}'",
                        this.m_Queue.Count,
                        this.m_CurrentReservation.Ticket,
                        this.m_CurrentReservation.BuildMachine
                        );
                }
#line default
                #endregion

                this.m_CurrentReservation.LastInquiry = DateTime.Now;
                return this.m_CurrentReservation.Status;
            }

            #region Tracing
#line hidden
            if (BuildMachineManager.m_TraceSwitch.TraceError)
            {
                Trace.TraceError(
                    "Issued ticket is no longer in queue (or never was).\nQueue Length = '{0}'\nTicket = '{1}'",
                    this.m_Queue.Count,
                    ticket
                    );
            }
#line default
            #endregion

            return ReservationStatus.Error;
        }

        public void Release(string ticket)
        {
            #region Tracing
#line hidden
            if (BuildMachineManager.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Releasing reservation.\nQueue Length = '{0}'\nTicket = '{1}'",
                    this.m_Queue.Count,
                    ticket
                    );
            }
#line default
            #endregion

            foreach (Reservation reservation in this.m_Queue)
            {
                if (reservation.Ticket == ticket)
                {
                    reservation.LastInquiry = DateTime.Now;
                    reservation.Status = ReservationStatus.Released;
                    return;
                }
            }

            if (this.m_CurrentReservation != null && this.m_CurrentReservation.Ticket == ticket)
            {
                this.m_CurrentReservation.LastInquiry = DateTime.Now;
                this.m_CurrentReservation.Status = ReservationStatus.Released;
                return;
            }
        }
    }
}
